import Vue from "../index.js";
import { isReserveTag } from "./utils.js";

/**
 * 初始渲染和后续更新的入口
 * @param {VNode} oldVnode 老的 VNode
 * @param {VNode} vnode 新的 VNode
 * @returns VNode 的真实 DOM 节点
 */
export default function patch(oldVnode, vnode) {
  // console.log("vnode ===> 新节点 ==> ", vnode);
  if (oldVnode && !vnode) {
    // 老节点存在，新节点不存在，销毁了
    return;
  }

  if (!oldVnode) {
    // 如果没有老节点，说明是初次渲染
    createElm(vnode);
  } else {
    // 更新
    debugger;
    if (oldVnode.nodeType) {
      // nodeType是真实节点携带的
      // 父节点
      const parent = oldVnode.parentNode;
      // 参考节点，即老的 vnode 的下一个节点 也就是 script标签，新节点要插在 script 的前面
      const referNode = oldVnode.nextSibling;
      // 创建元素 将vnode变成真是节点,并添加到父节点内
      createElm(vnode, parent, referNode);
      // 移除老的 vnode
      parent.removeChild(oldVnode);
    } else {
      // 更新 update
      patchVndoe(oldVnode, vnode);
    }
  }

  return vnode.elm;
}

/**
 * 创建元素
 * @param {*} vnode VNode
 * @param {*} parent VNode 的父节点，真实节点
 * @param {*} referNode 参考节点
 * @returns
 */
function createElm(vnode, parent, referNode) {
  // 记录节点的父节点 因为我们要将当前创建的元素 插入到父节点内
  vnode.parent = parent;
  // 创建自定义组件，如果是非组件，则会继续后面的流程
  if (createComponent(vnode)) return;
  const { attr, children, text } = vnode;
  if (text) {
    // text存在说明是文本节点
    // 创建文本节点，并插入到父节点内
    vnode.elm = createTextNode(vnode);
  } else {
    // 元素节点
    // 创建元素节点，在vnode上记录对应的dom节点
    vnode.elm = document.createElement(vnode.tag);
    // 给元素设置属性
    setAttribute(attr, vnode);
    // 递归创建子节点 子节点的父元素是当前得本身 也就是vnode.elm
    for (let i = 0, len = children.length; i < len; i++) {
      createElm(children[i], vnode.elm);
    }
  }
  // 如果存在 parent，表示知道是谁的子节点, 将创建的节点插入到父节点内
  // 如果没有表示是最外层的 因为#app是作用域,是最外层的,所以要将他插入到对应的位置,也就是获取他的同级元素,因为我们的作用域可以是任何地方
  if (parent) {
    const elm = vnode.elm;
    if (referNode) {
      // 如果是#app,就插入到下方兄弟元素的上方
      parent.insertBefore(elm, referNode);
    } else {
      // 否则插入父元素内
      parent.appendChild(elm);
    }
  }
}

/**
 * 创建文本节点
 * @param {*} textVNode 文本节点的 VNode
 */
function createTextNode(textVNode) {
  let { text } = textVNode,
    textNode = null;
  if (text.expression) {
    // 存在表达式，这个表达式的值是一个响应式数据
    const value = textVNode.context[text.expression];
    textNode = document.createTextNode(
      typeof value === "object" ? JSON.stringify(value) : String(value)
    );
  } else {
    // 纯文本
    textNode = document.createTextNode(text.text);
  }
  return textNode;
}

/**
 * 给节点设置属性
 * @param {*} attr 属性 Map 对象
 * @param {*} vnode
 */
function setAttribute(attrs, vnode) {
  // 遍历属性，如果是普通属性，直接设置，如果是指令，则特殊处理
  for (let name in attrs) {
    if (name === "vModel") {
      // v-model 指令
      const { tag, value } = attrs.vModel;
      setVModel(tag, value, vnode);
    } else if (name === "vBind") {
      // v-bind 指令
      setVBind(vnode);
    } else if (name === "vOn") {
      // v-on 指令
      setVOn(vnode);
    } else {
      // 普通属性
      vnode.elm.setAttribute(name, attrs[name]);
    }
  }
}

/**
 * v-model 的原理
 * @param {*} tag 节点的标签名
 * @param {*} value 属性值
 * @param {*} node 节点
 */
function setVModel(tag, value, vnode) {
  const { context: vm, elm } = vnode;
  // vm[value]
  if (tag === "select") {
    // 选择框
    // 下拉框，<select></select>
    Promise.resolve().then(() => {
      // 利用 promise 延迟设置，直接设置不行，
      // 因为这会儿 option 元素还没创建
      elm.value = vm[value];
    });
    elm.addEventListener("change", function () {
      vm[value] = elm.value;
    });
  } else if (tag === "input" && elm.type === "text") {
    // 文本框，<input type="text" />
    elm.value = vm[value];
    elm.addEventListener("input", function () {
      vm[value] = elm.value;
    });
  } else if (tag === "input" && vnode.elm.type === "checkbox") {
    // 多选框
    elm.checked = vm[value];
    elm.addEventListener("change", function () {
      vm[value] = elm.checked;
    });
  }
}

/**
 * v-bind 原理
 * @param {*} vnode
 */
function setVBind(vnode) {
  const {
    attr: { vBind },
    elm,
    context: vm,
  } = vnode;
  // 处理bind得绑定数据
  for (const attrName in vBind) {
    // v-bind:id="name"   变成id="123"
    elm.setAttribute(attrName, vm[vBind[attrName]]);
    // 删除掉  v-bind:id
    elm.removeAttribute(`v-bind:${attrName}`);
  }
}

/**
 * v-on 原理
 * @param {*} vnode
 */
function setVOn(vnode) {
  const {
    attr: { vOn },
    elm,
    context: vm,
  } = vnode;

  for (const eventName in vOn) {
    elm.addEventListener(eventName, function (...args) {
      // 调用方法，因为方法也被代理到vm上 可以通过 this. 来访问，例如 this.app()
      vm[vOn[eventName]].apply(vm, args);
    });
  }
}

/**
 * 创建自定义组件
 * @param {*} vnode
 */
function createComponent(vnode) {
  const {
    tag,
    context: {
      $options: { components },
    },
  } = vnode;
  // 判断是否是保留标签
  // components里面是很多子组件，获取到当前子组件得key，拿到他的配置项
  // vnode.tag 一定要存在,因为text内容 没有tag属性
  if (vnode.tag && !isReserveTag(tag)) {
    // 不是保留标签 开始处理子组件
    // 拿到子组件配置项里面的配置 然后初始化他
    const compOptions = components[tag];
    const compIns = new Vue(compOptions);
    // compIns 不会走 this.$mount() 渲染， 因为他没有el属性
    // console.log("vnode ==> ", vnode);
    // 将父组件的vnode拷贝存放到子组件实例上
    compIns._parentVnode = vnode;
    // 记录子组件得父节点信息 解析插槽的时候 需要用到,因为插槽会覆盖父元素
    // compIns._vnode.parent = vnode.parent;
    // 手动执行 先帮定值 后渲染
    compIns.$mount();
    // 将处理后的子节点，插入到父元素中 （代替子组件的位置）
    vnode.parent.appendChild(compIns._vnode.elm);
    return true;
  }
}

/**
 * 对比新老节点 oldVnode 和 vnode , 找出其中的不同，然后更新老节点
 * @param {*} oldVnode
 * @param {*} vnode
 */
function patchVndoe(oldVnode, vnode) {
  // 没有进行更新
  if (oldVnode === vnode) return;

  // 如果有更新 就将旧的 vnode 同步到新的vnode上
  vnode.elm = oldVnode.elm;

  // 拿到新老 vnode 得孩子节点
  const newChild = vnode.children;
  const oldChild = oldVnode.children;
  // 新节点不存在文本节点
  if (!vnode.text) {
    // 新老节点都存在子节点
    if (newChild && oldChild) {
      // diff处理
      updateChildren(newChild, oldChild);
    } else if (newChild && !oldChild) {
      // 新节点有孩子，老节点没孩子 新增节点
    } else if (oldChild && !newChild) {
      // 老节点有孩子 新节点没孩子 删除老节点
    }
  } else {
    // 如果存在文本节点
    if (vnode.text.expression) {
      // 文本中是否存在表达式
      // 获取表达式的新值 也就是更新后的新值
      const value = JSON.stringify(vnode.context[vnode.text.expression]);
      try {
        // 获取表达式旧值
        const oldValue = oldVnode.elm.textContent;
        if (value !== oldValue) {
          // 新表达式值和旧表达式数值不一样 更新
          oldVnode.elm.textContent = value;
        }
      } catch (error) {
        // 防止更新时候遇到插槽
      }
    } else {
      // 文本不存在表达式
    }
  }

  if (newChild === oldChild) {
  }
}

/**
 * diff算法 对比孩子节点，找出不同点，然后将不同点更新到老节点
 */
function updateChildren(newChild, oldChild) {
  // 四个游标 分别记录新老节点的遍历位置和总长度
  let newStartIdx = 0;
  let newEndIdx = newChild.length - 1;
  let oldStartIdx = 0;
  let oldEndIdx = oldChild.length - 1;

  // 遍历 循环遍历所有的新老节点,找出节点不一样的地方,然后更新
  // 做假设降低时间复杂度 两端对比
  while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) {
    // 获取新开始记录节点
    const newStartNdoe = newChild[newStartIdx];
    // 新结束节点
    const newEndNode = newChild[newEndIdx];
    // 老开始节点
    const oldStartNode = oldChild[oldStartIdx];
    // 老结束节点
    const oldEndNode = oldChild[oldEndIdx];
    // 假设
    // newCHILDREN  A B C D
    // oldCHILDREN  a b c d
    // 假设一: 新开始节点 和 老开始节点 是同一个节点
    // 假设二: 新开始节点 和 老结束节点 是同一个节点
    // 假设三: 新结束节点 和 老开始节点 是同一个节点
    // 假设四: 新结束节点 和 老结束节点 是同一个节点
    // 如果没有命中以上任何假设 就老老实实遍历
    if (someVnode(newStartNdoe, oldStartNode)) {
      // 命中假设一
      patchVndoe(oldStartNode, newStartNdoe);
      // 移动游标
      newStartIdx++;
      oldStartIdx++;
    } else if (someVnode(newStartNdoe, oldEndNode)) {
      // 命中假设二
      patchVndoe(oldEndNode, newStartNdoe);
      // 将老节点移动到新开始的位置
      oldEndNode.elm.parentNode.insertBefore(
        oldEndNode.elm,
        oldChild[newStartIdx]
      );
      // 移动游标
      newStartIdx++;
      oldEndIdx--;
    } else if (someVnode(newEndNode, oldStartNode)) {
      // 命中假设三
      patchVndoe(oldEndNode, newEndNode);
      // 将老节点移动到新结束的位置
      oldEndNode.elm.parentNode.insertBefore(
        oldEndNode.elm,
        oldChild[newEndIdx]
      );
      // 移动游标
      newEndIdx--;
      oldStartIdx++;
    } else if (someVnode(newEndNode, oldEndNode)) {
      // 命中假设四
      patchVndoe(oldEndNodem, newEndNode);
      // 移动游标
      newStartIdx--;
      oldStartIdx--;
    } else {
      // 都没命中 老老实实遍历
    }
  }

  // 跳出循环，说明有一个节点已经遍历结束们需要处理下后续的事情
  if (newStartIdx < newEndIdx) {
    // 说明老节点遍历结束， 则将剩余的新节点添加到dom中
  } else if (oldStartIdx < oldEndIdx) {
    // 说明新节点先遍历结束 则将剩余的老节点删除掉
  }
}

/**
 * 判断两个节点是否是同一个
 */
function someVnode(a, b) {
  // if (!a && !b) return false;
  return a && b && a.key === b.key && a.tag === b.tag;
}
